
package com.sfeir.gwt.webding.client.component.menu;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasAlignment;
import com.google.gwt.user.client.ui.HasVerticalAlignment;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.MultiWordSuggestOracle;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.StackPanel;
import com.google.gwt.user.client.ui.SuggestBox;
import com.google.gwt.user.client.ui.Tree;
import com.google.gwt.user.client.ui.TreeItem;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
import com.sfeir.gwt.webding.client.component.menu.event.HasHelpHandlers;
import com.sfeir.gwt.webding.client.component.menu.event.HasMenuItemSelectHandlers;
import com.sfeir.gwt.webding.client.component.menu.event.HelpEvent;
import com.sfeir.gwt.webding.client.component.menu.event.HelpHandler;
import com.sfeir.gwt.webding.client.component.menu.event.MenuItemSelectEvent;
import com.sfeir.gwt.webding.client.component.menu.event.MenuItemSelectHandler;


/**
 * Generic composite menu
 * 
 * @author sfeir
 */

public class Menu extends Composite implements HasHelpHandlers, HasMenuItemSelectHandlers {
    
    public static final String ITEM_SEPARATOR = ">>";
    
    // This is where all sub-menus are being added
    private VerticalPanel root;
    
    private SuggestBox searchBox;
    
    private ImagesMenu images;
    
    private ScrollPanel menu;
    
    private List<Item> children = new ArrayList<Item>();
    
    private String filter = null;
    
    private Boolean filterEnabled = false;
    
    private Boolean searchBarEnabled = true;
    
    private HorizontalPanel searchBar = null;
    
    private StackPanel panel;

    private final SuggestBoxOracle suggestBoxOracle = new SuggestBoxOracle();

    /**
     * Cree un nouveau menu par defaut : Avec une barre de recherche avec autocompletion
     */
    public Menu() {
        this(true, false, GWT.<ImagesMenu> create(ImagesMenu.class));
    }
    
    public Menu(Boolean searchBarEnabled, Boolean filterEnabled) {
        this(searchBarEnabled, filterEnabled, GWT.<ImagesMenu> create(ImagesMenu.class));
    }
    
    /**
     * @param searchBarEnabled Si la barre de recherche est affiche
     * @param filterEnabled Si le filtrage automatique du menu est actif
     * @param images Les images utilisées (le bouton d'aide)
     */
    public Menu(Boolean searchBarEnabled, Boolean filterEnabled, ImagesMenu images) {
        this.images = images;
        
        createWidget();
        initWidget(root);
        setSearchBarEnabled(searchBarEnabled);
        setFilterEnabled(filterEnabled);
    }
    
    private void createWidget() {
        root = new VerticalPanel();
        root.setWidth("100%");
        root.addStyleName("menu-root-panel");
        
        menu = new ScrollPanel();
        menu.addStyleName("menu-vertical-panel");
        menu.setSize("100%", "100%");
        
        
        panel = new StackPanel();
        panel.addStyleName("menu-stack-panel");
        panel.setSize("100%", "100%");
        menu.add(panel);
//        menu.setCellWidth(panel, "100%");
//        menu.setCellHeight(panel, "100%");
        
        root.add(menu);
        root.setCellWidth(menu, "100%");
        root.setCellHeight(menu, "100%");
    }
    
    /**
     * @param menuTitle Create a new menu with the given main title.
     */
    public Menu(String menuTitle) {
        this();
        
        HTML title = new HTML(menuTitle);
        title.addStyleName("menuTitle");
        
        root.insert(title, 0);
    }
    
    /**
     * Retourne le nombre de sous entree
     * 
     * @return Entier
     */
    public int getChildCount() {
        return children.size();
    }
    
    /**
     * Retourne la liste des sous entree du menu
     * 
     * @return
     */
    public List<Item> getChildren() {
        return children;
    }
    
    /**
     * Retourne la sous entree du menu a la position donnee
     * 
     * @param index
     * @return
     */
    public Item getChild(int index) {
        if ((index < 0) || (index >= children.size()))
            return null;
        return children.get(index);
    }
    
    /**
     * Ajoute une entree dans le menu
     * 
     * @param item
     */
    public void add(Item item) {
        adopt(item);
        children.add(item);
        addItem(item);
    }
    
    /**
     * Insere une entree dans le menu a la position donnee
     * 
     * @param item
     * @param index
     */
    public void insert(Item item, int index) {
        adopt(item);
        children.add(index, item);
        onItemChange();
    }

    /**
     * Adpte un item
     * @param item
     */
    @SuppressWarnings("deprecation")
    private void adopt(Item item) {
        item.setParent(null);
        item.setMenuParent(this);
    }
    
    /**
     * Enleve une entree du menu
     * 
     * @param item
     */
    public Item remove(Item item) {
        if (item != null && children.contains(item)) {
            orphan(item);
            children.remove(item);
            onItemChange();
            return item;
        }
        return null;
    }
    
    /**
     * Supprime une entree du menu a l'index donnee
     * 
     * @param index
     * @return
     */
    public Item remove(int index) {
        Item item = getChild(index);
        if (item != null) {
            orphan(item);
            children.remove(index);
            onItemChange();
        }
        return item;
    }
    
    /**
     * Supprime tous les sous menu
     * 
     * @param index
     * @return
     */
    public void removeAll() {
        for (int i = children.size() - 1; i >= 0; i--) {
            Item item = getChild(i);
            orphan(item);
            children.remove(i);
        }
        clear();
    }

    /**
     * Detache un element du menu
     * @param item
     */
    @SuppressWarnings("deprecation")
    private void orphan(Item item) {
        item.setParent(null);
        item.setMenuParent(null);
    }
    
    private Timer timerFilter = new Timer() {
        @Override
        public void run() {
            onItemChangeRun();
        }
    };
    
    /**
     * Reconstruit l'arbre, la reconstruction est retarde
     * Eviter de l'appeler directement
     */
    void onItemChange() {
        timerFilter.cancel();
        // Seulement si il y a des elements et que le menu est affiche
        if (children.size() > 0) {
            if (isAttached())
                timerFilter.schedule(200);
            else
                onItemChangeRun();
        }
    }
    
    /**
     * Reconstruit tous le menu lors de changement de la page
     */
    private void onItemChangeRun() {
        panel.setVisible(true);
        menu.setVisible(true);
        clear();
        openIndex = -1;
        for (Item item : children) {
            addItem(item);
        }
        if (openIndex != -1)
            panel.showStack(openIndex);
        else
            panel.showStack(0);
    }

    /**
     * Réinitialise l'affichage
     * Ne supprime pas tous les items (utiliser removeAll() pour cela)
     * 
     */
    public void clear() {
        timerFilter.cancel();
        panel.clear();
        suggestBoxOracle.clear();
    }
    
    /**
     * Reconstruit le menu
     * Non utile, peut arriver si la modification des items n'est pas parfaitement dispatcher
     */
    public void refresh() {
        onItemChange();
    }
    
    private int openIndex = -1;

    private void addItem(Item item) {
        Tree tree = new Tree();
        tree.setAnimationEnabled(true);
        tree.setSize("100%", "100%");
        tree.addSelectionHandler(new SelectionHandler<TreeItem>() {

            public void onSelection(SelectionEvent<TreeItem> event) {
                TreeItem selected = event.getSelectedItem();
                unSelectAll();
                selected.getTree().setSelectedItem(selected, false);
            }
            
        });
        for (Item child : item.getChildren()) {
            TreeItem buildTreeItem = buildTreeItem(child);
            if (buildTreeItem != null) {
                if (filterEnabled && filter != null)
                    buildTreeItem.setState(true);
                else
                    buildTreeItem.setState(child.isOpen());
                tree.addItem(buildTreeItem);
            }
        }
        panel.add(tree, buildHtmlLabelMenu(item), true);
        if (openIndex == -1)
            if (tree.getItemCount() != 0)
                openIndex = panel.getWidgetIndex(tree);
    }
    
    /**
     * Construit une sous partie de l'arbre
     * 
     * @param item
     * @return l'item de l'arbre, si il n'est pas filtree ou qu'un de ses enfants n'est pas filtree
     */
    private TreeItem buildTreeItem(Item item) {
        TreeItem treeItem = new TreeItem(buildLabelMenu(item));
        for (Item child : item.getChildren()) {
            TreeItem buildTreeItem = buildTreeItem(child);
            if (buildTreeItem != null) {
                treeItem.addItem(buildTreeItem);
                if (filterEnabled && filter != null)
                    buildTreeItem.setState(true);
            }
        }
        if (filterEnabled && treeItem.getChildCount() == 0 && !isFiltered(item))
            return null;
        return treeItem;
    }
    
    /**
     * retourne vrai si l'item est filtree : il est affiche
     * 
     * @param item
     * @return
     */
    private boolean isFiltered(Item item) {
        if (filter != null) {
            return item.isFiltered(filter);
        }
        return true;
    }
    
    /**
     * Construit le label du menu, ajoute l'evenement et insere une icone si elle est presente
     * 
     * @param item
     * @return
     */
    protected Widget buildLabelMenu(final Item item) {
        HorizontalPanel panel = new HorizontalPanel();
        panel.setVerticalAlignment(HasAlignment.ALIGN_MIDDLE);
        panel.addStyleName("menu-sub");
        
        Anchor linkMenu = new Anchor();
        linkMenu.addStyleName("menu-sub-title");
        
        linkMenu.setHTML(item.getLabel());
        linkMenu.addClickHandler(new ClickHandler() {
            
            public void onClick(ClickEvent event) {
                fireEvent(new MenuItemSelectEvent(item));
            }
        });
        if (item.getIcon() != null) {
            Image icon = new Image(item.getIcon(), 0, 0, 16, 16);
            panel.add(icon);
        }
        else {
            if (item.getIconImage() != null) {
                Image icon = item.getIconImage().createImage();
                panel.add(icon);
            }
        }
        panel.add(linkMenu);
        return panel;
    }
    
    protected String buildHtmlLabelMenu(Item item) {
        String label = item.getLabel();
        if (item.getIcon() != null) {
            Image icon = new Image(item.getIcon(), 0, 0, 16, 16);
            label = icon.toString() + label;
        }
        else {
            if (item.getIconImage() != null) {
                label = item.getIconImage().getHTML() + label;
            }
        }
        return label;
    }
    
    /**
     * Ajouter un evenement sur le clic sur le bouton help
     */
    public HandlerRegistration addHelpHandler(HelpHandler handler) {
        return addHandler(handler, HelpEvent.getType());
    }
    
    /**
     * Ajouter un evenement sur la selection d'une entree d'un menu
     */
    public HandlerRegistration addMenuItemSelectHandler(MenuItemSelectHandler handler) {
        return addHandler(handler, MenuItemSelectEvent.getType());
    }
    
    /**
     * Filtre tous le menu avec le texte donnee
     * 
     * @param text
     */
    public void setFilter(String text) {
        if (filterEnabled) {
            if (text != null && !text.trim().equals("")) {
                filter = text.trim().toLowerCase();
            }
            else {
                filter = null;
            }
            onItemChange();
        }
    }
    
    /**
     * Retourne le filtre actuel
     * 
     * @return
     */
    public String getFilter() {
        return filter;
    }
    
    /**
     * Retourne si le filtre automatique du menu est actif
     * 
     * @return
     */
    public Boolean getFilterEnabled() {
        return filterEnabled;
    }
    
    /**
     * Definie si le filtre automatique du menu est actif
     * 
     * @param filterEnabled
     */
    public void setFilterEnabled(Boolean filterEnabled) {
        this.filterEnabled = filterEnabled;
        suggestBoxOracle.setEnabled(!filterEnabled);
    }
    
    public Boolean getFullTextSearch() {
        return suggestBoxOracle.getFullTextSearch();
    }
    
    /**
     * Indique que la recherche des mots peut se faire dans tous le mot et non au début de
     * chaque mot
     * 
     * @param fullTextSearch
     */
    public void setFullTextSearch(Boolean fullTextSearch) {
        suggestBoxOracle.setFullTextSearch(fullTextSearch);
    }
    
    /**
     * Retourne si la barre de recherche est visible
     * 
     * @return
     */
    public Boolean getSearchBarEnabled() {
        return searchBarEnabled;
    }
    
    /**
     * Definie si la barre de recherche est visible Si la barre de recherche est visible mais que le
     * filtre automatique n'est pas actif, La barre de recherche est considere comme une barre de
     * suggestion
     * 
     * @param searchBarEnabled
     */
    public void setSearchBarEnabled(Boolean searchBarEnabled) {
        this.searchBarEnabled = searchBarEnabled;
        if (searchBarEnabled && searchBar == null) {
            searchBar = createSearchBar();
            root.insert(searchBar, 0);
            root.setCellWidth(searchBar, "100%");
            root.setCellWidth(searchBar, "20px");
        }
        else {
            if (!searchBarEnabled && searchBar != null) {
                root.remove(searchBar);
                searchBox = null;
                searchBar = null;
            }
        }
    }
    
    public Widget getSearchBar() {
        this.searchBarEnabled = true;
        return createSearchBar();
    }
    
    /**
     * Initialise la barre de recherche
     * 
     * @return
     */
    private HorizontalPanel createSearchBar() {
        HorizontalPanel searchBar = new HorizontalPanel();
        searchBox = new SuggestBox(suggestBoxOracle);
        searchBox.setAnimationEnabled(true);
        searchBox.addSelectionHandler(new SelectionHandler<Suggestion>() {
            public void onSelection(SelectionEvent<Suggestion> event) {
                launchMenu(searchBox.getText());
            };
        });
        searchBox.getTextBox().addKeyUpHandler(new KeyUpHandler() {
            public void onKeyUp(KeyUpEvent event) {
                if (event.isControlKeyDown() && event.getNativeKeyCode() == 67
                        || event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
                    searchBox.setText("");
                    setFilter(null);
                }
                else if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
                    //L'évènement Entrer et déjà gérer par GWT
                }
                else {
                    setFilter(searchBox.getText());
                }
            }
        });
        searchBox.setWidth("100%");
        searchBar.add(searchBox);
        searchBar.setCellWidth(searchBox, "100%");
        Image helpImage = images.help().createImage();
        helpImage.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                fireEvent(new HelpEvent());
            }
        });
        searchBar.add(helpImage);
        searchBar.setCellVerticalAlignment(helpImage, HasVerticalAlignment.ALIGN_MIDDLE);
        return searchBar;
    }
    
    protected void launchMenu(String text) {
        String[] path = text.split(ITEM_SEPARATOR);
        int level = 0;
        Item selectedItem = null;
        Tree tree = null;
        TreeItem treeItem = null;
        unSelectAll();
        while (level < path.length) {
            List<Item> list = selectedItem != null ? selectedItem.getChildren() : children;
            selectedItem = null;
            for (Item item : list) {
                if (path[level].equalsIgnoreCase(item.getLabel())) {
                    selectedItem = item;
                    int index = list.indexOf(item);
                    //Ouvre automatiquement le menu correspondant
                    if (!filterEnabled) {
                        if (level == 0) {
                            panel.showStack(index);
                            tree = (Tree) panel.getWidget(index);
                        }
                        else {
                            if (treeItem != null) {
                                treeItem = treeItem.getChild(index);
                            }
                            else if (tree != null) {
                                treeItem = tree.getItem(index);
                            }
                            if (treeItem != null) {
                                treeItem.setState(true);
                                if (level + 1 == path.length) {
                                    tree.setSelectedItem(treeItem, false);
                                    //treeItem.setSelected(true); //Selectionne le dernier noeud
                                }
                            }
                        }
                    }
                    level++;
                    break;
                }
            }
            if (selectedItem == null) // Non trouvee
                return;
        }
        if (selectedItem != null) {
            fireEvent(new MenuItemSelectEvent(selectedItem));
        }
    }
    
    private void unSelectAll() {
        unSelectAll(null);
    }
    
    private void unSelectAll(Tree selectedTree) {
        for (int i = 0; i < panel.getWidgetCount(); i++) {
            Tree tree = (Tree) panel.getWidget(i);
            if (tree != null && selectedTree != tree)
                tree.setSelectedItem(null, false);
        }
    }

    @Override
    protected void onAttach() {
        super.onAttach();
        onItemChange();
    }
    
    @Override
    public void setVisible(boolean visible) {
        super.setVisible(visible);
//        panel.setVisible(visible);
//        menu.setVisible(visible);
//        if (visible == true) {
//            setWidth(width);
//            onItemChange();
//        }
    }
    
    public void setWidth(int width) {
        //this.width = width;
        super.setWidth("96%");
//        panel.setWidth((width - 20) + "px");
//        menu.setWidth(width + "px");
    }
    
    public void setHeight(int height) {
        super.setHeight(height + "px");
        menu.setHeight(height + "px");
    }
    
    public class SuggestBoxOracle extends MultiWordSuggestOracle {
        private List<MultiWordSuggestion> list = new ArrayList<MultiWordSuggestion>();
        
        private Boolean buildList = false;
        
        private Boolean fullTextSearch = true;
        
        private Boolean enabled = true;
        
        public SuggestBoxOracle() {
            super(" " + ITEM_SEPARATOR);
        }
        
        public SuggestBoxOracle(Boolean fullTextSearch) {
            super(" " + ITEM_SEPARATOR);
            this.fullTextSearch = fullTextSearch;
        }
        
        @Override
        public void requestDefaultSuggestions(Request request, Callback callback) {
//            if (enabled && !buildDefaultList && children != null) {
//                buildDefaultResponse(children);
//            }
            super.requestDefaultSuggestions(request, callback);
        }
        
        @Override
        public void requestSuggestions(Request request, Callback callback) {
            if (!enabled) {
                clear();
                super.requestDefaultSuggestions(request, callback);
                return;
            }
            if (!buildList)
                build();
            if (fullTextSearch) {
                Response response = findSuggestions(request.getQuery(), request.getLimit());
                callback.onSuggestionsReady(request, response);
            }
            else {
                super.requestSuggestions(request, callback);
            }
        }
        
        /**
         * Reinitialise tous le composant
         */
        public void clear() {
            super.clear();
            list.clear();
            buildList = false;
            setDefaultSuggestions(new ArrayList<Suggestion>());
        }
        
        public Boolean getFullTextSearch() {
            return fullTextSearch;
        }
        
        /**
         * Indique que la recherche des mots peut se faire dans tous le mot et non au début de
         * chaque mot
         * 
         * @param fullTextSearch
         */
        public void setFullTextSearch(Boolean fullTextSearch) {
            this.fullTextSearch = fullTextSearch;
            clear();
        }
        
        /**
         * Construit les listes
         */
        private void build() {
            clear();
            if (fullTextSearch) {
                super.clear();
                buildFullTextSuggestionsList(children, null, null);
            }
            else {
                buildSuggestions(children, null);
            }
            buildList = true;
        }
        
        /**
         * Construit la liste pour MultiSuggestOracle
         * 
         * @param children
         */
        private void buildSuggestions(List<Item> children, String parent) {
            for (Item item : children) {
                String path = ((parent == null) ? "" : parent + ITEM_SEPARATOR) + item.getLabel();
                add(path);
                buildSuggestions(item.getChildren(), path);
            }
        }
        
        /**
         * Construit la liste pour la recherche fullText
         * 
         * @param children
         */
        private void buildFullTextSuggestionsList(List<Item> children, String parent, String label) {
            for (Item item : children) {
                String path = ((parent == null) ? "" : parent + ITEM_SEPARATOR) + item.getLabel();
                String display = ((label == null) ? "" : label + ITEM_SEPARATOR)
                    + buildHtmlLabelMenu(item);
                list.add(createSuggestion(path, display));
                buildFullTextSuggestionsList(item.getChildren(), path, display);
            }
        }
        
       /**
         * Recherche toutes les suggestions
         * 
         * @param query La requete
         * @param limit Le nombre de resultats 
         * @return
         */
        public Response findSuggestions(String query, int limit) {
            Collection<Suggestion> listResponse = new ArrayList<Suggestion>();
            query = query.trim().toLowerCase();
            for (MultiWordSuggestion item : list) {
                if (isFiltered(query, item)) {
                    listResponse.add(item);
                    if (--limit == 0)
                        break;
                }
            }
            return new Response(listResponse);
        }
        
        /**
         * Retourne si l'element match la recherche
         * @param query La recherche
         * @param item l'element
         * @return
         */
        private boolean isFiltered(String query, MultiWordSuggestion item) {
            String replacementString = item.getReplacementString();
            if (replacementString == null)
                return false;
            String filtered = replacementString.trim().toLowerCase();
            return filtered.contains(query);
        }

        public void setEnabled(Boolean enabled) {
            this.enabled = enabled;
            clear();
        }

        public Boolean getEnabled() {
            return enabled;
        }
    }
    
}
